Skip to main content

mobius_gateway/wire/
records.rs

1use super::*;
2
3/// Optional gateway broadcasts a client may suppress on its connection.
4/// Approvals, session events, errors, and request responses cannot be suppressed.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum GatewayNotification {
8    /// Unsolicited conversation catalog and activity updates.
9    Sessions,
10    /// Unsolicited Bot catalog updates.
11    Bots,
12}
13
14/// The computer view this connection can open.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum ComputerView {
18    /// No interactive computer view is available.
19    Unavailable,
20    /// The local app renders the gateway's assigned browser page.
21    EmbeddedBrowser,
22    /// The gateway serves its desktop through an authenticated stream.
23    RemoteDesktop,
24}
25
26/// Gateway-wide frontend-safe state sent after authentication.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct ReadyPayload {
29    /// Release version of the connected gateway process.
30    pub gateway_version: String,
31    /// The machine name.
32    pub machine_name: String,
33    /// The interactive computer view available to this connection.
34    pub computer_view: ComputerView,
35    /// The bots.
36    pub bots: Vec<BotRecord>,
37    /// The sessions.
38    pub sessions: Vec<SessionRecord>,
39    /// The background approvals.
40    pub background_approvals: Vec<BackgroundApproval>,
41    /// The providers.
42    pub providers: Vec<ProviderStatus>,
43    /// The provider instances.
44    pub provider_instances: Vec<ProviderInstance>,
45    /// The bot defaults.
46    pub bot_defaults: Option<VersionedAgentConfig>,
47    /// The models.
48    pub models: Vec<ModelChoice>,
49    /// The model providers.
50    pub model_providers: BTreeMap<String, String>,
51    /// The middleware features.
52    pub middleware_features: Vec<MiddlewareFeature>,
53    /// The extensions.
54    pub extensions: Vec<ExtensionRecord>,
55    /// The contributions.
56    pub contributions: Vec<FrontendContribution>,
57    /// The max active sessions.
58    pub max_active_sessions: usize,
59    /// The session file limits.
60    pub session_file_limits: SessionFileLimits,
61    /// Content revision of each cacheable section.
62    pub revisions: BTreeMap<ReadySection, String>,
63    /// Sections sent empty because the client holds them at these revisions or skips them.
64    pub omitted: BTreeSet<ReadySection>,
65}
66
67impl ReadyPayload {
68    /// Exchanges one cacheable section with `other`.
69    pub fn swap_section(&mut self, other: &mut Self, section: ReadySection) {
70        use std::mem::swap;
71        match section {
72            ReadySection::Config => {
73                swap(&mut self.providers, &mut other.providers);
74                swap(&mut self.provider_instances, &mut other.provider_instances);
75                swap(&mut self.bot_defaults, &mut other.bot_defaults);
76                swap(&mut self.models, &mut other.models);
77                swap(&mut self.model_providers, &mut other.model_providers);
78                swap(
79                    &mut self.middleware_features,
80                    &mut other.middleware_features,
81                );
82                swap(&mut self.extensions, &mut other.extensions);
83                swap(&mut self.contributions, &mut other.contributions);
84            }
85            ReadySection::Bots => swap(&mut self.bots, &mut other.bots),
86            ReadySection::Sessions => swap(&mut self.sessions, &mut other.sessions),
87        }
88    }
89
90    /// Moves the sections the gateway omitted back in from the catalog the client holds.
91    pub fn restore_omitted(&mut self, held: &mut Self) {
92        for section in std::mem::take(&mut self.omitted) {
93            self.swap_section(held, section);
94        }
95    }
96
97    /// Replaces this held catalog with a newer Ready, keeping the sections it omitted.
98    pub fn update(&mut self, mut next: Self) {
99        next.restore_omitted(self);
100        *self = next;
101    }
102
103    /// Content revision of one section, comparable across connections to one gateway build.
104    #[must_use]
105    pub fn revision(&self, section: ReadySection) -> String {
106        match section {
107            ReadySection::Config => content_revision(&(
108                &self.providers,
109                &self.provider_instances,
110                &self.bot_defaults,
111                &self.models,
112                &self.model_providers,
113                &self.middleware_features,
114                &self.extensions,
115                &self.contributions,
116            )),
117            ReadySection::Bots => content_revision(&self.bots),
118            ReadySection::Sessions => content_revision(&self.sessions),
119        }
120    }
121
122    /// The same gateway identity with no catalog content.
123    pub(crate) fn blank(&self) -> Self {
124        Self {
125            gateway_version: String::new(),
126            machine_name: String::new(),
127            computer_view: self.computer_view,
128            bots: Vec::new(),
129            sessions: Vec::new(),
130            background_approvals: Vec::new(),
131            providers: Vec::new(),
132            provider_instances: Vec::new(),
133            bot_defaults: None,
134            models: Vec::new(),
135            model_providers: BTreeMap::new(),
136            middleware_features: Vec::new(),
137            extensions: Vec::new(),
138            contributions: Vec::new(),
139            max_active_sessions: self.max_active_sessions,
140            session_file_limits: self.session_file_limits,
141            revisions: BTreeMap::new(),
142            omitted: BTreeSet::new(),
143        }
144    }
145}
146
147/// A stable digest of one catalog value's wire form.
148#[must_use]
149pub fn content_revision(value: &impl Serialize) -> String {
150    use sha2::Digest as _;
151    let mut hasher = sha2::Sha256::new();
152    // Serializing into a digest cannot fail for catalog records.
153    let _ = serde_json::to_writer(&mut hasher, value);
154    hasher.finalize()[..16]
155        .iter()
156        .map(|byte| format!("{byte:02x}"))
157        .collect()
158}
159
160/// A cacheable part of the Ready catalog.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum ReadySection {
164    /// Providers, models, Bot defaults, middleware, extensions and contributions.
165    Config,
166    /// The Bot catalog.
167    Bots,
168    /// The visible session catalog.
169    Sessions,
170}
171
172/// Every section a Ready payload can omit.
173pub const READY_SECTIONS: [ReadySection; 3] = [
174    ReadySection::Config,
175    ReadySection::Bots,
176    ReadySection::Sessions,
177];
178
179/// What a client already holds of the Ready catalog, sent with `authenticate`.
180#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
181pub struct CatalogHint {
182    /// Sections the client holds, by the revision the gateway reported.
183    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
184    pub known: BTreeMap<ReadySection, String>,
185    /// Sections this connection never needs, such as on a file-transfer connection.
186    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
187    pub skip: BTreeSet<ReadySection>,
188}
189
190impl CatalogHint {
191    /// A hint that names no section.
192    #[must_use]
193    pub fn is_empty(&self) -> bool {
194        self.known.is_empty() && self.skip.is_empty()
195    }
196}
197
198/// One position of the session catalog in a [`ServerMessage::SessionsChanged`].
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200#[serde(untagged)]
201pub enum SessionSlot {
202    /// A session unchanged since the catalog this connection last received, by ID.
203    Unchanged(String),
204    /// A new or changed session.
205    Changed(Box<SessionRecord>),
206}
207
208/// Rebuilds the catalog `sessions` held from a [`ServerMessage::SessionsChanged`].
209pub fn apply_session_changes(sessions: &mut Vec<SessionRecord>, changes: Vec<SessionSlot>) {
210    let mut held: Vec<_> = std::mem::take(sessions).into_iter().map(Some).collect();
211    *sessions = changes
212        .into_iter()
213        .filter_map(|slot| match slot {
214            SessionSlot::Changed(session) => Some(*session),
215            SessionSlot::Unchanged(id) => held
216                .iter_mut()
217                .find(|held| held.as_ref().is_some_and(|held| held.session_id == id))
218                .and_then(Option::take),
219        })
220        .collect();
221}
222
223/// `+added −removed` lines of a Git diff, as Git's `--numstat` counts them.
224#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
225pub struct DiffTotals {
226    /// Added lines.
227    pub additions: u64,
228    /// Removed lines.
229    pub deletions: u64,
230}
231
232/// One hidden Bot conversation currently waiting for a human execution decision.
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct BackgroundApproval {
235    /// The session identifier.
236    pub session_id: String,
237    /// The bot identifier.
238    pub bot_id: String,
239    /// The turn identifier.
240    pub turn_id: String,
241    /// The request identifier.
242    pub request_id: String,
243}
244
245/// Frontend-safe state for one opened session.
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct SessionReadyPayload {
248    /// The active turn identifiers.
249    pub active_turn_ids: Vec<String>,
250    /// The pending approvals.
251    pub pending_approvals: Vec<mobius::protocol::ExecApprovalRequestEvent>,
252    /// The latest sequence.
253    pub latest_sequence: u64,
254    /// The next before sequence.
255    pub next_before_sequence: Option<u64>,
256    /// The workspace.
257    pub workspace: Option<WorkspaceInfo>,
258    /// The attached folders.
259    pub attached_folders: Vec<PathBuf>,
260    /// The git.
261    pub git: Option<GitStatus>,
262    /// The session.
263    pub session: SessionConfiguredEvent,
264    /// The contributions.
265    pub contributions: Vec<FrontendContribution>,
266    /// The widgets.
267    pub widgets: Vec<SessionWidget>,
268    /// The tool count.
269    pub tool_count: usize,
270    /// The compaction count.
271    pub compaction_count: u64,
272    /// The context limit tokens.
273    pub context_limit_tokens: Option<i64>,
274    /// The active message delivery.
275    pub active_message_delivery: mobius::protocol::ActiveMessageDelivery,
276    /// The run stats.
277    pub run_stats: RunStats,
278}
279
280/// One currently mounted capability widget and its owning namespace.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct SessionWidget {
283    /// The capability.
284    pub capability: String,
285    /// The item.
286    pub item: FrontendWidget,
287}
288
289/// One visible session with gateway-owned catalog presentation metadata.
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291pub struct SessionRecord {
292    /// The session identifier.
293    pub session_id: String,
294    /// The session context.
295    pub session_context: mobius::protocol::SessionContext,
296    /// The parent session identifier.
297    pub parent_session_id: Option<String>,
298    /// The parent sequence.
299    pub parent_sequence: Option<u64>,
300    /// The sequence.
301    pub sequence: u64,
302    /// The first user message.
303    pub first_user_message: Option<String>,
304    /// The execution stats.
305    pub execution_stats: mobius::backend::checkpoint::ExecutionStats,
306    /// The title.
307    pub title: Option<String>,
308    /// The pinned.
309    pub pinned: bool,
310    /// The activity.
311    pub activity: SessionActivity,
312    /// The created at.
313    pub created_at: i64,
314    /// The updated at.
315    pub updated_at: i64,
316}
317
318/// Gateway-observed lifecycle state for one session.
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(deny_unknown_fields)]
321pub struct SessionActivity {
322    /// Pending capability attention items, independent of turn activity.
323    pub attention: u32,
324    /// The state.
325    pub state: SessionActivityState,
326    /// The turn identifier.
327    pub turn_id: Option<String>,
328    /// The approval request identifier.
329    pub approval_request_id: Option<String>,
330    /// The started at.
331    pub started_at: Option<i64>,
332    /// The last outcome.
333    pub last_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
334    /// The message.
335    pub message: Option<String>,
336}
337
338impl Default for SessionActivity {
339    fn default() -> Self {
340        Self {
341            attention: 0,
342            state: SessionActivityState::Idle,
343            turn_id: None,
344            approval_request_id: None,
345            started_at: None,
346            last_outcome: None,
347            message: None,
348        }
349    }
350}
351
352/// Current work state advertised in the session catalog.
353#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(rename_all = "snake_case")]
355pub enum SessionActivityState {
356    /// Selects the idle case.
357    Idle,
358    /// Selects the running case.
359    Running,
360    /// Selects the awaiting approval case.
361    AwaitingApproval,
362}
363
364/// Canonical workspace identity and path for one chat.
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366pub struct WorkspaceInfo {
367    /// The identifier.
368    pub id: String,
369    /// The path.
370    pub path: PathBuf,
371}
372
373/// Local branch state for a Git-backed workspace.
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375pub struct GitStatus {
376    /// The current branch.
377    pub current_branch: String,
378    /// The branches.
379    pub branches: Vec<String>,
380}
381
382/// Public metadata for one SSH identity found on the gateway host.
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(deny_unknown_fields)]
385pub struct SshIdentityRecord {
386    /// The label.
387    pub label: String,
388    /// The algorithm.
389    pub algorithm: String,
390    /// The fingerprint.
391    pub fingerprint: String,
392}
393
394/// One explicit Git patch selection.
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "snake_case")]
397pub enum GitDiffScope {
398    /// Selects the staged case.
399    Staged,
400    /// Selects the unstaged case.
401    Unstaged,
402    /// Selects the committed case.
403    Committed,
404}
405
406/// Which openable files to include in a workspace catalog.
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "snake_case")]
409pub enum WorkspaceFileScope {
410    /// Selects the modified case.
411    Modified,
412    /// Selects the all case.
413    All,
414}
415
416/// One regular file confined to the selected chat workspace.
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub struct WorkspaceFileRecord {
419    /// The path.
420    pub path: String,
421    /// The size.
422    pub size: u64,
423}
424
425/// One bounded folder listing from the gateway host.
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct DirectoryListing {
428    /// The path.
429    pub path: PathBuf,
430    /// The parent.
431    pub parent: Option<PathBuf>,
432    /// The entries.
433    pub entries: Vec<DirectoryEntry>,
434}
435
436/// A selectable child folder on the gateway host.
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438pub struct DirectoryEntry {
439    /// The name.
440    pub name: String,
441    /// The path.
442    pub path: PathBuf,
443    /// The is directory.
444    pub is_directory: bool,
445}
446
447/// A frontend-safe agent composition guarded by an optimistic revision.
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449pub struct VersionedAgentConfig {
450    /// The revision.
451    pub revision: u64,
452    /// The config.
453    pub config: AgentComposition,
454}
455
456/// Runtime settings an authenticated client may read and replace atomically.
457#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
458#[serde(deny_unknown_fields)]
459pub struct AgentComposition {
460    /// The provider.
461    pub provider: ProviderConfig,
462    /// The realtime voice.
463    pub realtime_voice: Option<String>,
464    /// The middleware.
465    pub middleware: MiddlewareConfig,
466    /// The extensions.
467    pub extensions: BTreeSet<String>,
468    /// The system prompt.
469    pub system_prompt: String,
470    /// The max model steps.
471    pub max_model_steps: u64,
472}
473
474/// Package format of one gateway-managed extension.
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case")]
477pub enum ExtensionKind {
478    /// Selects the skill case.
479    Skill,
480    /// Selects the plugin case.
481    Plugin,
482}
483
484/// One executable plugin hook shown before digest-bound trust is granted.
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
486#[serde(deny_unknown_fields)]
487pub struct ExtensionHookRecord {
488    /// The event.
489    pub event: String,
490    /// The matcher.
491    pub matcher: Option<String>,
492    /// The command.
493    pub command: String,
494    /// The timeout seconds.
495    pub timeout_seconds: u64,
496}
497
498/// Frontend-safe metadata for one installed extension snapshot.
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(deny_unknown_fields)]
501pub struct ExtensionRecord {
502    /// The identifier.
503    pub id: String,
504    /// The capability.
505    pub capability: String,
506    /// The kind.
507    pub kind: ExtensionKind,
508    /// The name.
509    pub name: String,
510    /// The description.
511    pub description: String,
512    /// The version.
513    pub version: Option<String>,
514    /// The source.
515    pub source: String,
516    /// The reference.
517    pub reference: Option<String>,
518    /// The subdirectory.
519    pub subdirectory: Option<String>,
520    /// The resolved revision.
521    pub resolved_revision: String,
522    /// The digest.
523    pub digest: String,
524    /// The skills.
525    pub skills: Vec<String>,
526    /// The hooks.
527    pub hooks: Vec<ExtensionHookRecord>,
528    /// The hooks trusted.
529    pub hooks_trusted: bool,
530}
531
532/// Provider and model settings. Credentials are resolved only on the gateway host.
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534#[serde(deny_unknown_fields)]
535pub struct ProviderConfig {
536    /// Stable identity of one configured setup of `provider`. A gateway may hold
537    /// several instances of the same provider with separate credentials.
538    pub instance: String,
539    /// The provider.
540    pub provider: String,
541    /// The model.
542    pub model: String,
543    #[serde(default, skip_serializing_if = "Option::is_none")]
544    /// The base URL.
545    pub base_url: Option<String>,
546    /// The endpoint auth.
547    pub endpoint_auth: ProviderEndpointAuth,
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    /// The reasoning effort.
550    pub reasoning_effort: Option<String>,
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    /// Optional native Responses processing tier.
553    pub service_tier: Option<String>,
554    /// The web search.
555    pub web_search: HostedWebSearch,
556}
557
558/// Authentication applied when calling one configured provider endpoint.
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
560#[serde(rename_all = "snake_case")]
561pub enum ProviderEndpointAuth {
562    /// Selects the provider default case.
563    ProviderDefault,
564    /// Selects the credentialless case.
565    Credentialless,
566}
567
568/// Credential availability exposed without returning credential material.
569#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570pub struct ProviderStatus {
571    /// The provider.
572    pub provider: String,
573    /// The label.
574    pub label: String,
575    /// The symbol.
576    pub symbol: FrontendSymbol,
577    /// The description.
578    pub description: String,
579    /// The model identifiers configurable.
580    pub model_ids_configurable: bool,
581    /// The auth.
582    pub auth: ProviderAuthKind,
583    /// The default base URL.
584    pub default_base_url: Option<String>,
585    /// Custom roots implement this provider's native image and voice APIs.
586    pub native_custom_endpoints: bool,
587    /// The default API key env.
588    pub default_api_key_env: Option<String>,
589    /// The models.
590    pub models: Vec<ProviderModel>,
591    /// The web search.
592    pub web_search: Vec<FrontendSettingOption>,
593    /// The tool discovery.
594    pub tool_discovery: ToolDiscoveryMode,
595    /// The custom endpoint tool discovery.
596    pub custom_endpoint_tool_discovery: Option<ToolDiscoveryMode>,
597    /// The realtime voices.
598    pub realtime_voices: Vec<String>,
599}
600
601/// User-chosen accent for distinguishing provider instances in model selectors.
602#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
603#[serde(rename_all = "snake_case")]
604pub enum ProviderTint {
605    #[default]
606    /// Selects the blue case.
607    Blue,
608    /// Selects the teal case.
609    Teal,
610    /// Selects the green case.
611    Green,
612    /// Selects the yellow case.
613    Yellow,
614    /// Selects the orange case.
615    Orange,
616    /// Selects the red case.
617    Red,
618    /// Selects the purple case.
619    Purple,
620    /// Selects the white case.
621    White,
622}
623
624/// The silhouette of a Bot's face.
625#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
626#[serde(rename_all = "snake_case")]
627pub enum BotShape {
628    /// Selects the circle case.
629    Circle,
630    /// Selects the squircle case.
631    Squircle,
632    /// Selects the triangle case.
633    Triangle,
634    /// Selects the diamond case.
635    Diamond,
636    /// Selects the hexagon case.
637    Hexagon,
638    /// Selects the star case.
639    Star,
640    /// Selects the flower case.
641    Flower,
642}
643
644impl BotShape {
645    /// Every shape, in the order pickers show them.
646    pub const ALL: [Self; 7] = [
647        Self::Circle,
648        Self::Squircle,
649        Self::Triangle,
650        Self::Diamond,
651        Self::Hexagon,
652        Self::Star,
653        Self::Flower,
654    ];
655}
656
657/// One durable setup of a provider. Several may share one `provider`.
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659pub struct ProviderInstance {
660    /// The label.
661    pub label: String,
662    /// The tint.
663    pub tint: ProviderTint,
664    /// The configured.
665    pub configured: bool,
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    /// The credential hint.
668    pub credential_hint: Option<String>,
669    /// The selection.
670    pub selection: ProviderConfig,
671    /// The model identifiers.
672    pub model_ids: Vec<String>,
673    /// The reasoning efforts.
674    pub reasoning_efforts: Vec<String>,
675}
676
677/// Frontend type attached to one authenticated connection.
678#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
679#[serde(rename_all = "snake_case")]
680pub enum ClientKind {
681    /// Selects the CLI case.
682    Cli,
683    /// Selects the macos case.
684    Macos,
685    /// Selects the ios case.
686    Ios,
687    /// Selects the ipados case.
688    Ipados,
689    /// Selects the gateway dashboard case.
690    GatewayDashboard,
691}
692
693/// One paired client and its current connection state.
694#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
695pub struct ClientStatus {
696    /// The client identifier.
697    pub client_id: String,
698    /// The label.
699    pub label: String,
700    /// The kinds.
701    pub kinds: Vec<ClientKind>,
702    /// The connections.
703    pub connections: usize,
704}
705
706impl ProviderStatus {
707    #[must_use]
708    /// Returns the realtime voices.
709    pub fn realtime_voices(&self, base_url: Option<&str>) -> &[String] {
710        if self.native_custom_endpoints
711            || mobius::backend::model::provider::uses_default_endpoint(
712                self.default_base_url.as_deref(),
713                base_url,
714            )
715        {
716            &self.realtime_voices
717        } else {
718            &[]
719        }
720    }
721
722    #[must_use]
723    /// Returns the configurable base URL.
724    pub fn configurable_base_url(&self) -> bool {
725        self.default_base_url.is_some()
726    }
727
728    #[must_use]
729    /// Returns the default model.
730    pub fn default_model(&self) -> Option<&ProviderModel> {
731        self.models.first()
732    }
733}
734
735/// One model advertised by a provider manifest.
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737pub struct ProviderModel {
738    /// The identifier.
739    pub id: String,
740    /// The label.
741    pub label: String,
742    /// The description.
743    pub description: String,
744    /// The context window.
745    pub context_window: i64,
746    /// The reasoning.
747    pub reasoning: Vec<ReasoningChoice>,
748    /// The default reasoning.
749    pub default_reasoning: Option<String>,
750    /// The tool discovery.
751    pub tool_discovery: ToolDiscoveryMode,
752}
753
754/// One reasoning effort advertised for a provider model.
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct ReasoningChoice {
757    /// The identifier.
758    pub id: String,
759    /// The label.
760    pub label: String,
761    /// The description.
762    pub description: String,
763}
764
765/// Frontend-safe provider authentication mechanism.
766#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
767#[serde(rename_all = "snake_case")]
768pub enum ProviderAuthKind {
769    /// Selects the API key case.
770    ApiKey,
771    /// Selects the device code case.
772    DeviceCode,
773}
774
775/// Enabled optional middleware IDs and their schema-backed settings.
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
777#[serde(deny_unknown_fields)]
778pub struct MiddlewareConfig {
779    pub(crate) enabled: BTreeSet<String>,
780    /// The settings.
781    pub settings: BTreeMap<String, BTreeMap<String, FrontendSettingValue>>,
782}
783
784impl MiddlewareConfig {
785    /// Returns the selected policy that excludes an optional capability.
786    #[must_use]
787    pub fn disabled_by<'a>(
788        &self,
789        features: &'a [mobius::protocol::MiddlewareFeature],
790        id: &str,
791        selected_model: Option<&mobius::protocol::ModelChoice>,
792    ) -> Option<&'a str> {
793        if let Some(capability) = features
794            .iter()
795            .find(|feature| feature.id == id)
796            .and_then(|feature| feature.required_model_capability)
797            && selected_model.is_some_and(|model| !model.supports(capability))
798        {
799            return Some(match capability {
800                mobius::protocol::ModelCapability::ImageGeneration => {
801                    "a model without image generation"
802                }
803                mobius::protocol::ModelCapability::RealtimeVoice => {
804                    "a model without realtime voice"
805                }
806            });
807        }
808        features
809            .iter()
810            .filter(|feature| feature.required || self.enabled(&feature.id))
811            .find_map(|feature| {
812                feature.settings.iter().find_map(|setting| {
813                    let mobius::protocol::FrontendSettingKind::Select { options, .. } =
814                        &setting.kind
815                    else {
816                        return None;
817                    };
818                    let Some(FrontendSettingValue::String(value)) =
819                        self.setting(&feature.id, &setting.id)
820                    else {
821                        return None;
822                    };
823                    options
824                        .iter()
825                        .find(|option| {
826                            option.value == *value
827                                && option.disables.iter().any(|disabled| disabled == id)
828                        })
829                        .map(|option| option.label.as_str())
830                })
831            })
832    }
833
834    /// Applies exclusions advertised by the currently selected policies.
835    pub fn reconcile(
836        &mut self,
837        features: &[mobius::protocol::MiddlewareFeature],
838        selected_model: Option<&mobius::protocol::ModelChoice>,
839    ) {
840        let excluded = self
841            .enabled
842            .iter()
843            .filter(|id| self.disabled_by(features, id, selected_model).is_some())
844            .cloned()
845            .collect::<Vec<_>>();
846        for id in excluded {
847            self.enabled.remove(&id);
848        }
849    }
850
851    /// Returns whether one advertised optional middleware is enabled.
852    #[must_use]
853    pub fn enabled(&self, id: &str) -> bool {
854        self.enabled.contains(id)
855    }
856
857    /// Updates one advertised optional middleware before gateway validation.
858    pub fn set_enabled(&mut self, id: impl Into<String>, enabled: bool) {
859        let id = id.into();
860        if enabled {
861            self.enabled.insert(id);
862        } else {
863            self.enabled.remove(&id);
864        }
865    }
866
867    /// Returns one advertised middleware setting.
868    #[must_use]
869    pub fn setting(&self, middleware: &str, setting: &str) -> Option<&FrontendSettingValue> {
870        self.settings.get(middleware)?.get(setting)
871    }
872
873    /// Sets or clears one advertised middleware setting before gateway validation.
874    pub fn set_setting(
875        &mut self,
876        middleware: impl Into<String>,
877        setting: impl Into<String>,
878        value: Option<FrontendSettingValue>,
879    ) {
880        let middleware = middleware.into();
881        let setting = setting.into();
882        if let Some(value) = value {
883            self.settings
884                .entry(middleware)
885                .or_default()
886                .insert(setting, value);
887        } else if let Some(settings) = self.settings.get_mut(&middleware) {
888            settings.remove(&setting);
889            if settings.is_empty() {
890                self.settings.remove(&middleware);
891            }
892        }
893    }
894
895    pub(crate) fn entries(&self) -> impl Iterator<Item = &str> {
896        self.enabled.iter().map(String::as_str)
897    }
898}
899
900/// Capability-rendered preview whose inner events remain provider-neutral.
901#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
902pub struct RenderedPreview {
903    /// The semantic preview symbol.
904    pub symbol: Option<FrontendSymbol>,
905    /// Accumulated completed duration in milliseconds.
906    pub duration_ms: Option<u64>,
907    /// Start of the current activity, when still running.
908    pub started_at_ms: Option<i64>,
909    /// The identifier.
910    pub id: String,
911    /// The title.
912    pub title: String,
913    /// The subtitle.
914    pub subtitle: String,
915    /// The page identifier.
916    pub page_id: String,
917    /// The update.
918    pub update: FrontendPreviewUpdate,
919    /// The events.
920    pub events: Vec<RenderedEvent>,
921    /// The next.
922    pub next: Option<Op>,
923}
924
925/// One preview event and its capability-rendered blocks.
926#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
927pub struct RenderedEvent {
928    /// The submission identifier.
929    pub submission_id: Option<String>,
930    /// The recorded at milliseconds.
931    pub recorded_at_ms: i64,
932    /// The event.
933    pub event: EventMsg,
934    /// The blocks.
935    pub blocks: Vec<RenderedBlock>,
936}
937
938/// One timestamped semantic event and its deterministic presentation.
939#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
940pub struct RecordedEvent {
941    /// The sequence.
942    pub sequence: u64,
943    /// The recorded at milliseconds.
944    pub recorded_at_ms: i64,
945    /// The event.
946    pub event: Event,
947    /// The stream metrics.
948    pub stream_metrics: Vec<StreamMetrics>,
949    /// The blocks.
950    pub blocks: Vec<RenderedBlock>,
951    /// The preview.
952    pub preview: Option<RenderedPreview>,
953}
954
955/// Gateway-owned profile and aggregate usage information.
956#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
957pub struct ProfileSnapshot {
958    /// The user name.
959    pub user_name: Option<String>,
960    /// The daily usage.
961    pub daily_usage: Vec<DailyUsage>,
962    /// The provider usage.
963    pub provider_usage: Vec<ProviderUsage>,
964    /// The run stats.
965    pub run_stats: RunStats,
966    /// The recent run groups.
967    pub recent_run_groups: Vec<SessionRunGroup>,
968}
969
970/// One configured provider's remote subscription usage.
971#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
972pub struct ProviderUsage {
973    /// The provider.
974    pub provider: String,
975    /// The limits.
976    pub limits: Option<Vec<UsageLimit>>,
977    /// The error.
978    pub error: Option<String>,
979}
980
981/// Recent executions grouped under their nearest visible session.
982#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
983pub struct SessionRunGroup {
984    /// The session identifier.
985    pub session_id: String,
986    /// The title.
987    pub title: String,
988    /// The runs.
989    pub runs: Vec<RunSummary>,
990}
991
992/// Completed execution totals plus the active run, when one exists.
993#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
994pub struct RunStats {
995    #[serde(flatten)]
996    /// The completed.
997    pub completed: mobius::backend::checkpoint::ExecutionStats,
998    /// The active.
999    pub active: Option<RunSummary>,
1000}
1001
1002/// Frontend-safe summary of one completed or active user turn.
1003#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1004pub struct RunSummary {
1005    /// The session identifier.
1006    pub session_id: String,
1007    /// The submission identifier.
1008    pub submission_id: String,
1009    /// The turn identifier.
1010    pub turn_id: String,
1011    /// The started at milliseconds.
1012    pub started_at_ms: i64,
1013    /// The finished at milliseconds.
1014    pub finished_at_ms: Option<i64>,
1015    /// The elapsed milliseconds.
1016    pub elapsed_ms: u64,
1017    /// The outcome.
1018    pub outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1019    /// The model calls.
1020    pub model_calls: u64,
1021    /// The tool calls.
1022    pub tool_calls: u64,
1023    /// The failed tool calls.
1024    pub failed_tool_calls: u64,
1025    /// The usage.
1026    pub usage: TokenUsage,
1027}
1028
1029/// Usage accrued during one Unix day.
1030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1031pub struct DailyUsage {
1032    /// The unix day.
1033    pub unix_day: u64,
1034    /// The provider.
1035    pub provider: String,
1036    /// The usage.
1037    pub usage: TokenUsage,
1038}
1039
1040/// One durable Bot profile.
1041#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1042pub struct BotRecord {
1043    /// Canonical project-free conversation, opened through the ordinary session API.
1044    pub conversation_session_id: String,
1045    /// The identifier.
1046    pub id: String,
1047    /// The handle.
1048    pub handle: String,
1049    /// The name.
1050    pub name: String,
1051    /// The description.
1052    pub description: String,
1053    /// The tint.
1054    pub tint: ProviderTint,
1055    /// The face's silhouette.
1056    pub shape: BotShape,
1057    /// The config.
1058    pub config: VersionedAgentConfig,
1059    /// The accepts file attachments.
1060    pub accepts_file_attachments: bool,
1061    /// The routine interaction policy.
1062    pub routine_interaction_policy: RoutineInteractionPolicy,
1063}
1064
1065/// Whether unattended routine work can stop for a human execution decision.
1066#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1067#[serde(rename_all = "snake_case")]
1068pub enum RoutineInteractionPolicy {
1069    /// Selects the unattended case.
1070    Unattended,
1071    /// Selects the may pause for approval case.
1072    MayPauseForApproval,
1073}
1074
1075/// One Bot-owned routine.
1076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct Routine {
1078    /// The identifier.
1079    pub id: String,
1080    /// The bot identifier.
1081    pub bot_id: String,
1082    /// The workspace.
1083    pub workspace: PathBuf,
1084    /// The instructions.
1085    pub instructions: String,
1086    /// User-authored event and timer bindings.
1087    pub bindings: Vec<RoutineBinding>,
1088    /// The enabled.
1089    pub enabled: bool,
1090    /// The finished.
1091    pub finished: bool,
1092    /// The next run at.
1093    pub next_run_at: Option<i64>,
1094}
1095
1096/// A user-selected scheduling rule.
1097#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1098#[serde(deny_unknown_fields)]
1099pub struct RoutineSchedule {
1100    /// The kind.
1101    pub kind: RoutineScheduleKind,
1102    /// The at.
1103    pub at: Option<i64>,
1104    /// The every seconds.
1105    pub every_seconds: Option<u64>,
1106    /// The expression.
1107    pub expression: Option<String>,
1108    /// The time zone.
1109    pub time_zone: Option<String>,
1110}
1111
1112/// The supported scheduling rule families.
1113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1114#[serde(rename_all = "snake_case")]
1115pub enum RoutineScheduleKind {
1116    /// Selects the once case.
1117    Once,
1118    /// Selects the interval case.
1119    Interval,
1120    /// Selects the cron case.
1121    Cron,
1122}
1123
1124/// A read-only page of a Bot routine transcript.
1125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1126pub struct RoutineRunPreview {
1127    /// The routine.
1128    pub routine: Routine,
1129    /// The run.
1130    pub run: RoutineRun,
1131    /// The records.
1132    pub records: Vec<RecordedEvent>,
1133    /// The next before sequence.
1134    pub next_before_sequence: Option<u64>,
1135}
1136
1137/// One completed or active invocation of a Bot routine.
1138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1139pub struct RoutineRun {
1140    /// The identifier.
1141    pub id: String,
1142    /// The routine identifier.
1143    pub routine_id: String,
1144    /// The bot identifier.
1145    pub bot_id: String,
1146    /// The started at.
1147    pub started_at: i64,
1148    /// The finished at.
1149    pub finished_at: Option<i64>,
1150    /// The status.
1151    pub status: RoutineRunStatus,
1152    /// The session identifier.
1153    pub session_id: Option<String>,
1154    /// The message.
1155    pub message: Option<String>,
1156}
1157
1158/// Durable state of one routine invocation.
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1160#[serde(rename_all = "snake_case")]
1161pub enum RoutineRunStatus {
1162    /// Selects the running case.
1163    Running,
1164    /// Selects the succeeded case.
1165    Succeeded,
1166    /// Selects the failed case.
1167    Failed,
1168    /// Selects the skipped case.
1169    Skipped,
1170    /// The user stopped the invocation.
1171    Cancelled,
1172}
1173
1174/// Editable routine content and its user-authorized bindings.
1175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1176#[serde(deny_unknown_fields)]
1177pub struct RoutineDefinition {
1178    /// Explicit execution workspace.
1179    pub workspace: PathBuf,
1180    /// Saved task instructions.
1181    pub instructions: String,
1182    /// Event and timer triggers with exact actions.
1183    pub bindings: Vec<RoutineBinding>,
1184}
1185
1186/// One user-authored trigger on its containing routine.
1187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1188#[serde(deny_unknown_fields)]
1189pub struct HookBinding<A> {
1190    /// Stable binding identity within the routine.
1191    pub id: String,
1192    /// Exact event or timer trigger.
1193    pub on: HookSelector,
1194    /// Action on this routine.
1195    pub action: A,
1196}
1197
1198/// A trigger whose action addresses its containing routine.
1199pub type RoutineBinding = HookBinding<RoutineAction>;
1200
1201/// Typed actions shared by reporting and control subscriptions.
1202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1203#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1204pub enum BotAction {
1205    /// Deliver the saved reporting mandate to the Bot conversation.
1206    Report {
1207        /// User-authored mandate.
1208        instruction: String,
1209    },
1210    /// Invoke the ordinary routine command handler.
1211    Routine {
1212        /// Addressed command.
1213        command: RoutineCommand,
1214    },
1215    /// Submit an ordinary message or interrupt to an owned session.
1216    Session {
1217        /// Owned destination.
1218        session_id: String,
1219        /// Existing core operation.
1220        op: Box<mobius::protocol::Op>,
1221    },
1222}
1223
1224/// Finite commands shared by UI, tools, timers and event bindings.
1225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1226#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1227pub enum RoutineAction {
1228    /// Start one invocation.
1229    Start,
1230    /// Stop only the identified invocation.
1231    Stop {
1232        /// Invocation identity.
1233        run_id: String,
1234    },
1235    /// Disable future starts without stopping an active run.
1236    Pause,
1237    /// Enable future starts from the current time.
1238    Resume,
1239    /// Delete this routine through the ordinary cleanup path.
1240    Delete,
1241    /// Replace the editable definition.
1242    Update {
1243        /// Validated replacement.
1244        definition: RoutineDefinition,
1245    },
1246}
1247
1248/// A command addressed to one routine.
1249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1250#[serde(deny_unknown_fields)]
1251pub struct RoutineCommand {
1252    /// Target routine identity.
1253    pub routine_id: String,
1254    /// Typed action.
1255    pub action: RoutineAction,
1256}
1257
1258/// Gateway-established origin of a committed hook event.
1259#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1260#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1261pub enum HookSource {
1262    /// A Bot command.
1263    Bot {
1264        /// Owner identity.
1265        bot_id: String,
1266    },
1267    /// An owned routine.
1268    Routine {
1269        /// Routine identity.
1270        routine_id: String,
1271    },
1272    /// An owned session.
1273    Session {
1274        /// Session identity.
1275        session_id: String,
1276    },
1277    /// A timer on an owned routine.
1278    Schedule {
1279        /// Routine identity.
1280        routine_id: String,
1281        /// Binding identity.
1282        binding_id: String,
1283    },
1284    /// A gateway lifecycle operation.
1285    Gateway,
1286    /// An authenticated client.
1287    Client {
1288        /// Paired client identity.
1289        client_id: String,
1290    },
1291}
1292
1293/// Exact trigger shared by routine bindings and reporting consumers.
1294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1295#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1296pub enum HookSelector {
1297    /// The existing schedule model, owned by this binding.
1298    Schedule {
1299        /// Timer rule.
1300        schedule: RoutineSchedule,
1301        /// Optional cutoff.
1302        ends_at: Option<i64>,
1303    },
1304    /// An exact source and typed boundary, with applicable outcome filters.
1305    Event {
1306        /// Source identity.
1307        source: HookSource,
1308        /// Boundary.
1309        kind: HookKind,
1310        /// Optional terminal routine outcome.
1311        routine_outcome: Option<RoutineRunStatus>,
1312        /// Optional terminal session outcome.
1313        session_outcome: Option<mobius::backend::checkpoint::ExecutionOutcome>,
1314        /// Optional custom event name.
1315        custom_name: Option<String>,
1316    },
1317}
1318
1319/// Finite lifecycle boundaries; source payloads cannot introduce commands.
1320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1321#[serde(rename_all = "snake_case")]
1322pub enum HookKind {
1323    /// Routine registered.
1324    RoutineCreated,
1325    /// Routine definition replaced.
1326    RoutineUpdated,
1327    /// Routine disabled.
1328    RoutinePaused,
1329    /// Routine enabled.
1330    RoutineResumed,
1331    /// Routine removed.
1332    RoutineDeleted,
1333    /// Invocation reserved.
1334    RunStarted,
1335    /// Invocation completed, failed or was cancelled.
1336    RunFinished,
1337    /// Invocation skipped before starting.
1338    RunSkipped,
1339    /// A binding's timer became due.
1340    ScheduleDue,
1341    /// Session created.
1342    SessionCreated,
1343    /// A turn began.
1344    SessionTurnStarted,
1345    /// A turn reached a durable outcome.
1346    SessionTurnFinished,
1347    /// A turn awaits a human decision.
1348    SessionApproval,
1349    /// A capability requests a new user decision.
1350    SessionAttention,
1351    /// Session deleted.
1352    SessionDeleted,
1353    /// Session owner changed.
1354    SessionOwnerChanged,
1355    /// Client authenticated.
1356    ClientConnected,
1357    /// Client disconnected.
1358    ClientDisconnected,
1359    /// A Bot emitted a named event.
1360    CustomReceived,
1361}
1362
1363/// One durable gateway event used by all hook consumers.
1364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1365#[serde(deny_unknown_fields)]
1366pub struct HookEvent {
1367    /// Stable event identity.
1368    pub id: String,
1369    /// Authenticated or internally established source.
1370    pub source: HookSource,
1371    /// Immediate causing event, if any.
1372    pub cause_id: Option<String>,
1373    /// Bounded prior event chain used to prevent feedback.
1374    pub ancestry: Vec<String>,
1375    /// Owning Bot.
1376    pub bot_id: String,
1377    /// Unix timestamp in seconds.
1378    pub occurred_at: i64,
1379    /// Small typed lifecycle fact.
1380    pub data: HookData,
1381}
1382
1383/// Typed event facts, without executable source-provided instructions.
1384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1385#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
1386pub enum HookData {
1387    /// Routine registered.
1388    RoutineCreated {
1389        /// Routine identity.
1390        routine_id: String,
1391    },
1392    /// Routine definition changed.
1393    RoutineUpdated {
1394        /// Routine identity.
1395        routine_id: String,
1396    },
1397    /// Routine disabled.
1398    RoutinePaused {
1399        /// Routine identity.
1400        routine_id: String,
1401    },
1402    /// Routine enabled.
1403    RoutineResumed {
1404        /// Routine identity.
1405        routine_id: String,
1406    },
1407    /// Routine removed.
1408    RoutineDeleted {
1409        /// Routine identity.
1410        routine_id: String,
1411    },
1412    /// Invocation reserved.
1413    RunStarted {
1414        /// Routine identity.
1415        routine_id: String,
1416        /// Invocation identity.
1417        run_id: String,
1418        /// Execution transcript.
1419        session_id: Option<String>,
1420    },
1421    /// Invocation completed, failed or was cancelled.
1422    RunFinished {
1423        /// Routine identity.
1424        routine_id: String,
1425        /// Invocation identity.
1426        run_id: String,
1427        /// Terminal outcome.
1428        status: RoutineRunStatus,
1429        /// Execution transcript.
1430        session_id: Option<String>,
1431        /// Failure or cancellation reason.
1432        reason: Option<String>,
1433    },
1434    /// An overlapping or unavailable invocation was skipped.
1435    RunSkipped {
1436        /// Routine identity.
1437        routine_id: String,
1438        /// Skipped invocation identity.
1439        run_id: String,
1440        /// Saved reason.
1441        reason: String,
1442    },
1443    /// A binding's timer became due.
1444    ScheduleDue {
1445        /// Binding identity.
1446        binding_id: String,
1447    },
1448    /// A session was created.
1449    SessionCreated {
1450        /// Session identity.
1451        session_id: String,
1452    },
1453    /// A session turn began.
1454    SessionTurnStarted {
1455        /// Session identity.
1456        session_id: String,
1457        /// Turn identity.
1458        turn_id: String,
1459    },
1460    /// A session turn reached its committed outcome.
1461    SessionTurnFinished {
1462        /// Session identity.
1463        session_id: String,
1464        /// Turn identity.
1465        turn_id: String,
1466        /// Core execution outcome.
1467        outcome: mobius::backend::checkpoint::ExecutionOutcome,
1468    },
1469    /// A session awaits an execution decision.
1470    SessionApproval {
1471        /// Session identity.
1472        session_id: String,
1473        /// Turn identity.
1474        turn_id: String,
1475        /// Approval identity.
1476        request_id: String,
1477    },
1478    /// A capability requests a user decision.
1479    SessionAttention {
1480        /// Session identity.
1481        session_id: String,
1482        /// Owning capability.
1483        capability: String,
1484        /// Stable item identity.
1485        item_id: String,
1486        /// Bounded user-facing question or decision.
1487        text: String,
1488    },
1489    /// Session removed.
1490    SessionDeleted {
1491        /// Session identity.
1492        session_id: String,
1493    },
1494    /// Session transferred to another Bot.
1495    SessionOwnerChanged {
1496        /// Session identity.
1497        session_id: String,
1498        /// Previous Bot identity.
1499        previous_bot_id: String,
1500    },
1501    /// Paired client authenticated.
1502    ClientConnected {
1503        /// Paired client identity.
1504        client_id: String,
1505    },
1506    /// Paired client disconnected.
1507    ClientDisconnected {
1508        /// Paired client identity.
1509        client_id: String,
1510    },
1511    /// A Bot emitted a named JSON event.
1512    CustomReceived {
1513        /// User-selected event name.
1514        name: String,
1515        /// Bounded untrusted JSON evidence.
1516        data: serde_json::Value,
1517    },
1518}
1519
1520impl HookData {
1521    /// The selector boundary for this fact.
1522    #[must_use]
1523    pub fn kind(&self) -> HookKind {
1524        match self {
1525            Self::RoutineCreated { .. } => HookKind::RoutineCreated,
1526            Self::RoutineUpdated { .. } => HookKind::RoutineUpdated,
1527            Self::RoutinePaused { .. } => HookKind::RoutinePaused,
1528            Self::RoutineResumed { .. } => HookKind::RoutineResumed,
1529            Self::RoutineDeleted { .. } => HookKind::RoutineDeleted,
1530            Self::RunStarted { .. } => HookKind::RunStarted,
1531            Self::RunFinished { .. } => HookKind::RunFinished,
1532            Self::RunSkipped { .. } => HookKind::RunSkipped,
1533            Self::ScheduleDue { .. } => HookKind::ScheduleDue,
1534            Self::SessionCreated { .. } => HookKind::SessionCreated,
1535            Self::SessionTurnStarted { .. } => HookKind::SessionTurnStarted,
1536            Self::SessionTurnFinished { .. } => HookKind::SessionTurnFinished,
1537            Self::SessionApproval { .. } => HookKind::SessionApproval,
1538            Self::SessionAttention { .. } => HookKind::SessionAttention,
1539            Self::SessionDeleted { .. } => HookKind::SessionDeleted,
1540            Self::SessionOwnerChanged { .. } => HookKind::SessionOwnerChanged,
1541            Self::ClientConnected { .. } => HookKind::ClientConnected,
1542            Self::ClientDisconnected { .. } => HookKind::ClientDisconnected,
1543            Self::CustomReceived { .. } => HookKind::CustomReceived,
1544        }
1545    }
1546}
1547
1548/// User-authored reporting consumer of the same typed hook stream.
1549#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1550#[serde(deny_unknown_fields)]
1551pub struct BotSubscription {
1552    /// Owning Bot.
1553    pub bot_id: String,
1554    /// Exact trigger and its saved action.
1555    pub binding: HookBinding<BotAction>,
1556    /// Whether future matching facts should report.
1557    pub enabled: bool,
1558}